package com.atguigu.gmall.index.aspect;

import com.alibaba.fastjson.JSON;
import com.atguigu.gmall.index.annotation.GmallCache;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

@Component //组件类
@Aspect //切面类
@Slf4j
public class GmallCacheAspect {
    @Resource
    RedisTemplate redisTemplate;
    @Resource
    RedissonClient redissonClient;
    @Resource
    RBloomFilter<Long> bloomFilter;
//    @Before()
//    @After()
//    @AfterThrowing
//    @AfterReturning
    //需求：缓存管理前后增强  使用环绕通知
    //1、指定环绕通知绑定的方法：
   // @Around(value = "execution(* com.atguigu.gmall.index.service.impl.IndexServiceImpl.loadLevel23Cates(..))")
    //2、通过注解绑定增强的方法
    @Around(value = "@annotation(com.atguigu.gmall.index.annotation.GmallCache)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        //先获取目标方法上的注解对象和它的参数
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Object[] args = joinPoint.getArgs();//目标方法的参数列表
        Method method = signature.getMethod();//获取目标方法对象
        GmallCache gmallCache = method.getAnnotation(GmallCache.class);//获取方法上的指定注解对象
        String lockKey = gmallCache.lock();
        String prefix = gmallCache.prefix();
        int timeout = gmallCache.timeout();
        int random = gmallCache.random();
        String cid = ArrayUtils.isEmpty(args)?"": Arrays.asList(args).get(0).toString();
        //缓存穿透问题： 缓存空值可以避免使用不存在的相同的key频繁恶意攻击
        // 如果恶意攻击的客户端切换不同的不存在的key恶意攻击：缓存管理的业务执行较复杂 每个不同的key都会执行若干次
        //   redis操作+一次数据库操作：  最终redis缓存数据较多+ 一个请求后端处理较慢 导致 qps能力下降

        //可以判断 如果请求访问的key一定不存在 返回空值  如果存在执行后续的业务
        // 项目初始化时：将数据库存在的数据(以后作为redis的key缓存的数据) 加载到内存中缓存 (set缓存)
        // 查询缓存业务执行时：先去判断查询的key的数据是否存在(set的方法判断)
        // set的缺点： 以后数据较多 cid 较长，set占用较大内存 占用资源
        if(StringUtils.isNotEmpty(cid)&& !bloomFilter.contains(Long.parseLong(cid))){
            log.info("布隆过滤器判断数据不存在....{}",cid);
            return null;//布隆过滤器认为数据不存在 返回空
        }

        //1、先查询缓存是否存在： 缓存prefix+cid拼接 去redis中查询
        Object cache = redisTemplate.opsForValue().get(prefix + cid);
        if(cache!=null){
            //2、如果存在缓存直接返回
            log.info("查询到{}缓存返回数据:{}" , cid , JSON.toJSONString(cache));
            return cache;
        }
        //3、如果没有缓存：
        //4、加分布式锁：解决缓存击穿
        RLock lock = redissonClient.getLock(lockKey + ":" + cid);
        lock.lock();
        try {
            //5、再判断是否有缓存
            cache = redisTemplate.opsForValue().get(prefix + cid);
            if(cache!=null){
                //6、有缓存直接返回
                log.info("查询到{}缓存返回数据2:{}" , cid , JSON.toJSONString(cache));
                return cache;
            }
            //7、没有缓存调用目标方法得到结果
            //获取方法的返回值类型
            Object result = joinPoint.proceed(args);//执行目标方法：
            //8、存到缓存中：缓存添加随机时间 解决缓存雪崩问题
            //判断如果查询到的是空值 存到redis中的时间设置稍短： 解决缓存穿透问题
            if(result==null || (result instanceof List && ((List)result).size()==0) ){
                redisTemplate.opsForValue()
                        .set(prefix + cid , result,random
                                , TimeUnit.MINUTES);
            }else{
                redisTemplate.opsForValue()
                        .set(prefix + cid , result,timeout+new Random().nextInt(random)
                                , TimeUnit.MINUTES);
            }
            //9、返回结果
            log.info("查询数据库{}返回数据3:{}" , cid , JSON.toJSONString(result));
            return result;
        } finally {
            //释放锁：
            lock.unlock();
        }
    }
}
